Conversation
🦋 Changeset detectedLatest commit: 54acbc0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Looks great, but what about deserialization those objects? |
That's what the createSerialized$ does |
|
Does this mean we might be able to serialize functions from other libraries, like vanilla js? |
aca1de8 to
47942f4
Compare
|
@Varixo @thejackshelton gaah I had forgotten to push the actual commit 😅 |
Yes exactly. You need to add a symbol prop that serializes the value and then provide a deserializer that will get called lazily. |
47942f4 to
9009520
Compare
commit: |
built with Refined Cloudflare Pages Action⚡ Cloudflare Pages Deployment
|
9009520 to
103e3c1
Compare
165ac6d to
804f01d
Compare
804f01d to
9070c30
Compare
| : obj instanceof ComputedSignal && | ||
| !(obj instanceof SerializerSignal) && | ||
| (obj.$invalid$ || fastSkipSerialize(obj)) | ||
| ? NEEDS_COMPUTATION | ||
| : obj.$untrackedValue$; | ||
| if (v !== NEEDS_COMPUTATION) { | ||
| discoveredValues.push(v); | ||
| if (obj instanceof SerializerSignal) { | ||
| promises.push( | ||
| (obj.$computeQrl$ as any as QRLInternal<SerializerArg<any, any>>) | ||
| .resolve() | ||
| .then((arg) => { | ||
| let data; | ||
| if ((arg as any).serialize) { | ||
| data = (arg as any).serialize(v); | ||
| } | ||
| if (data === undefined) { | ||
| data = NEEDS_COMPUTATION; | ||
| } | ||
| serializationResults.set(obj, data); | ||
| discoveredValues.push(data); | ||
| }) | ||
| ); | ||
| } else { | ||
| discoveredValues.push(v); | ||
| } |
There was a problem hiding this comment.
I wonder if we could organise it somehow? It is very complicated right now.
What about doing just
if (obj instanceof WrappedSignal) {
} else if (obj instanceof ComputedSignal) {
} else if ...
thejackshelton
left a comment
There was a problem hiding this comment.
Very thorough PR. Awesome work ⚡
| * | ||
| * This function must not return a promise. | ||
| */ | ||
| deserialize: (data: S | undefined, previous: T | undefined) => T; |
There was a problem hiding this comment.
So on first render, the previous param is -> initial data / inital (3rd argument param of useSerializer$)?
There was a problem hiding this comment.
on first run, the data is initial|undef. After serialization, data is what was serialized.
If the function uses reactive scope, it will rerun and then current will be defined and data will be undefined.
| * | ||
| * If you do not provide it, the object will be serialized as `undefined`. | ||
| */ | ||
| serialize?: (customObject: T) => S | Promise<S>; |
There was a problem hiding this comment.
What's an example of when you may need the deserialize function but not the serialize function?
There was a problem hiding this comment.
When you're just constructing an object from scope data and you don't have any state to serialize.
|
I tried the changes locally and I'm having the following issue when using props as initial data: Plus a linting issue when using the Link to MRE: https://github.com/ianlet/qwik-custom-serder-scope-issues/blob/main/src/components/qwik-mapbox.tsx Edit: actually we won't be able to |
|
Also, after playing with it a little bit, I think the So, IMO it should be 2 different callbacks as it is 2 different use cases, triggered after 2 different events. |
9070c30 to
d67bb36
Compare
|
I addressed all the comments I think. |
|
@ianlet BTW even when only using mapbox on the client, this is still useful. Just make the deserialize |
d67bb36 to
5126138
Compare
I tried to use the new syntax with Also, having to handle the const mapbox = useSerializer$<
mapboxgl.Map | undefined,
SerializationState | undefined
>(() => ({
initial: {
token: token.value,
mapStyle: mapStyle.value,
center: center.value,
},
serialize: (map: mapboxgl.Map | undefined) => {
if (!map) return;
return {
token,
mapStyle: map.getStyle() || undefined,
center: map.getCenter().toArray(),
};
},
deserialize: (state: SerializationState | undefined) => {
if (isServer) return;
const s = state || { token: token.value, mapStyle: mapStyle.value, center: center.value };
return new mapboxgl.Map({
container: ref.value!,
style: s.mapStyle,
center: s.center,
});
},
update: (map: mapboxgl.Map | undefined) => {
if (!map) return;
map.setStyle(mapStyle.value);
map.setCenter(center.value);
},
}));Note: ESLint is still not happy about tracking the useTask$(({ track }) => {
const map = track(mapbox);
map?.on("load", () => console.log("MAP LOADED"));
}); |
|
I played more with it and if you change a value that is tracked more than once, the serialized value is only updated the first time: Playground #1 So I tried to change it to directly use the signal in the |
There was a problem hiding this comment.
PR Overview
This PR introduces custom serialization support by adding two new symbols (NoSerializeSymbol and SerializerSymbol) to control object serialization and by providing a new SerializerSignal type that enables custom serialization logic. Key changes include updates to the serialization and signal modules, added unit tests demonstrating custom serialization, and comprehensive documentation updates.
Reviewed Changes
| File | Description |
|---|---|
| packages/qwik/src/core/shared/shared-serialization.unit.ts | Adds unit tests for NoSerializeSymbol and SerializerSymbol, as well as SerializerSignal behavior. |
| packages/qwik/src/core/signal/signal.ts | Introduces type alias and minor refactoring for computed and wrapped signals, including read-only value enforcement. |
| packages/qwik/src/core/shared/shared-serialization.ts | Updates to support SerializerSignal in both allocation and serialization flows. |
| packages/qwik/src/core/signal/signal.public.ts & signal-api.ts | Exposes new createSerializer$ and createSerializerQrl APIs with improved type definitions and internal casting. |
| Documentation and changeset files | Updates to document the two new symbols and the custom serialization API. |
Copilot reviewed 32 out of 32 changed files in this pull request and generated no comments.
Comments suppressed due to low confidence (2)
packages/qwik/src/core/shared/shared-serialization.ts:312
- [nitpick] Consider expanding this comment to further clarify why SerializerSignal is always marked as invalid, ensuring future maintainers understand the rationale behind recreating the custom object during deserialization.
// The serialized signal is always invalid so it can recreate the custom object
packages/qwik/src/core/signal/signal-api.ts:32
- [nitpick] Consider refining the type casting here to reduce the double 'any' cast and make the conversion to QRLInternal<SerializerArg<T, S>> more type-safe.
return new SerializerSignal<T, S>(null, arg as any as QRLInternal<SerializerArg<T, S>>);
mhevery
left a comment
There was a problem hiding this comment.
Very excited about this change. Left few clarifing questions.
packages/eslint-plugin-qwik/tests/valid-lexical-scope/valid-no-serialize-symbol.tsx
Outdated
Show resolved
Hide resolved
d76b001 to
d7f7793
Compare
Co-authored-by: Varixo <admin@varixo.pl> Co-authored-by: Miško Hevery <misko@hevery.com>
d7f7793 to
4d056d4
Compare
|
@ianlet Thank you for your playgrounds, the problem is that you didn't use I added an eslint rule (yey vibe coding) that warns about this. |

This PR adds symbols to mark objects as (no/yes)serializable via symbols instead of via a Set, adds a symbol prop that will get called on serialization for custom serialization, and provides a new Signal type that lazily manages a non-serializable value.
Note that this is basically the same as
useComputed$, except that it is invalidated during SSR so that when the value gets read on the client it will always run the compute function.Also note that
useComputed$now passes the previous value to the compute function, to keep the implementation compact. This seems like it might be useful, should we document this?Everything works, looking for comments on implementation, naming etc.
TODO